package com.smartalarmclock.Model;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import com.smartalarmclock.Model.Exceptions.DataEntityDoesNotExistException;
import com.smartalarmclock.Model.Exceptions.InvalidDeleteDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.InvalidNullReferenceOnDeleteException;
import com.smartalarmclock.Model.Exceptions.InvalidReferenceToRemoveException;
import com.smartalarmclock.Model.Exceptions.InvalidSaveDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.ReferenceCannotBeNullException;
import com.smartalarmclock.Model.Exceptions.RingtoneDoesNotExistException;
import com.smartalarmclock.Model.Reference.DeleteMethod;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.media.Ringtone;
import android.media.RingtoneManager;

final class AlarmClock extends ReferencedDataEntity implements IAlarmClock, IReferencingDataEntity {

	// Database Constants
	
	public static final String AlarmClockTableName = "AlarmClock";
	
	private static final String c_AlarmClockColumnNameSettings = "Settings";
	private static final String c_AlarmClockColumnNameShutOffMethod = "Method";
	private static final String c_AlarmClockColumnNameRingtone = "Ringtone";
	private static final String c_AlarmClockColumnNameSnooze = "Snooze";
	
	private static final String[] c_QueryAllColumns = new String[] {
		c_ColumnNameId, c_AlarmClockColumnNameSettings, c_AlarmClockColumnNameShutOffMethod, c_AlarmClockColumnNameRingtone, c_AlarmClockColumnNameSnooze };
	
	// Members
	
	private static final boolean c_DefaultActiveStatus = true;
	private static final boolean c_DefaultRingStatus = true;
	private static final boolean c_DefaultVibrateStatus = true;
	private static final int c_DefaultRingtoneId = 1;
	private static final ShutOffMethod c_DefaultShutOffMethod = ShutOffMethod.Snooze;
	
	private EnumSet<Settings> m_Settings;
	private ShutOffMethod m_Method;
	private int m_RingtoneId;
	private final SingleReference<ISnooze> m_SnoozeReference; 
	
	// Constructors
	
	/**
	 * Public Constructor for new instances
	 * @param method the shut off method of the alarm clock
	 * @param isActive indicates if the alarm clock is active
	 * @param isRinging indicates if the alarm clock should ring,
	 * @param isVibrating indicates if the alarm clock should vibrate
	 * @param ringtoneId the id of the ringtone to use for the alarm clock
	 * @param snooze the Snooze instance for the alarm clock if "Snooze" is used
	 */
	protected AlarmClock(
		ShutOffMethod method,
		boolean isActive,
		boolean isRinging,
		boolean isVibrating,
		int ringtoneId,
		ISnooze snooze) {
		
		super();
		m_SnoozeReference = new SingleReference<ISnooze>(this, snooze, true, Snooze.class, DeleteMethod.Delete);
		Initialize(method, isActive, isRinging, isVibrating, ringtoneId, snooze);
	}
	
	/**
	 * Public Constructor for new instances, using all default values
	 */
	protected AlarmClock() {
		super();
		m_SnoozeReference = new SingleReference<ISnooze>(this, Snooze.class, DeleteMethod.Delete);
		Initialize(c_DefaultShutOffMethod, c_DefaultActiveStatus, c_DefaultRingStatus, c_DefaultVibrateStatus, c_DefaultRingtoneId, null);
	}
	
	/**
	 * Private Constructor for instances loaded from the DB.
	 * @param id the existing id of the instance in the DB.
	 * @param method the shut off method of the alarm clock
	 * @param isActive indicates if the alarm clock is active
	 * @param isRinging indicates if the alarm clock should ring,
	 * @param isVibrating indicates if the alarm clock should vibrate
	 * @param ringtoneId the id of the ringtone to use for the alarm clock
	 * @param snooze the Snooze instance for the alarm clock if "Snooze" is used
	 */
	private AlarmClock(
		int id,
		ShutOffMethod method,
		boolean isActive,
		boolean isRinging,
		boolean isVibrating,
		int ringtoneId,
		ISnooze snooze) {
		super(id);
		m_SnoozeReference = new SingleReference<ISnooze>(this, snooze, true, Snooze.class, DeleteMethod.Delete);
		Initialize(method, isActive, isRinging, isVibrating, ringtoneId, snooze);
	}
	
	// IAlarmClock implementation
	
	/**
	 * @return true if the alarm clock is active, otherwise false
	 */
	@Override
	public boolean IsActive() {
		return m_Settings.contains(Settings.Active);
	}
	
	/**
	 * Sets the active status of the alarm clock
	 * @param isActive the new active status of the alarm clock
	 */
	@Override
	public void SetActiveStatus(boolean isActive) {
		if (isActive) {
			m_Settings.add(Settings.Active);
		}
		else {
			m_Settings.remove(Settings.Active);
		}
		SetUpdated();
	}
	
	/**
	 * @return true if the alarm clock should ring, otherwise false
	 */
	@Override
	public boolean IsRinging() {
		return m_Settings.contains(Settings.Ring);
	}
	
	/**
	 * Sets the ring status of the alarm clock
	 * @param isRinging the new ring status of the alarm clock
	 */
	@Override
	public void SetRingStatus(boolean isRinging) {
		if (isRinging) {
			m_Settings.add(Settings.Ring);
		}
		else {
			m_Settings.remove(Settings.Ring);
		}
		SetUpdated();
	}
	
	/**
	 * @return true if the alarm clock should vibrate, otherwise false
	 */
	@Override
	public boolean IsVibrating() {
		return m_Settings.contains(Settings.Vibrate);
	}
	
	/**
	 * Sets the vibrate status of the alarm clock
	 * @param isVibrating the new vibrate status of the alarm clock
	 */
	@Override
	public void SetVibrateStatus(boolean isVibrating) {
		if (isVibrating) {
			m_Settings.add(Settings.Vibrate);
		}
		else {
			m_Settings.remove(Settings.Vibrate);
		}
		SetUpdated();
	}
	
	/**
	 * @return the shut off method of the alarm clock
	 */
	@Override
	public ShutOffMethod GetShutOffMethod() {
		return m_Method;
	}
	
	/**
	 * Sets the shut off method of the alarm clock
	 * @param method the new shut off method of the alarm clock
	 */
	@Override
	public void SetShutOffMethod(ShutOffMethod method) {
		m_Method = method;
		SetUpdated();
	}
	
	/**
	 * @return the ringtone id of the alarm clock
	 */
	@Override
	public int GetRingtoneId() {
		return m_RingtoneId;
	}
	
	/**
	 * @param context the context of the request (the originating Activity)
	 * @return the a Ringtone instance matching the ringtone id of the alarm clock
	 * @throws RingtoneDoesNotExistException if the ringtone was not found in the phone database
	 */
	@Override
	public Ringtone GetRingtone(Context context) throws RingtoneDoesNotExistException {
		RingtoneManager manager = new RingtoneManager(context);
		Cursor cursor = manager.getCursor();
		
		while (cursor.moveToNext()) {
			if (cursor.getInt(RingtoneManager.ID_COLUMN_INDEX) == m_RingtoneId) {
				return manager.getRingtone(cursor.getPosition());
			}
		}
		
		throw new RingtoneDoesNotExistException(m_RingtoneId);
	}
	
	/**
	 * Sets the ringtone of the alarm clock
	 * @param context the context of the request (the originating Activity)
	 * @param ringtoneTitle the title of the new ringtone as it is saved in the phone database
	 * @throws RingtoneDoesNotExistException if the ringtone was not found in the phone database
	 */
	@Override
	public void SetRingtone(Context context, String ringtoneTitle) throws RingtoneDoesNotExistException {
		RingtoneManager manager = new RingtoneManager(context);
		Cursor cursor = manager.getCursor();
		
		while (cursor.moveToNext()) {
			if (cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX) == ringtoneTitle) {
				m_RingtoneId = cursor.getInt(RingtoneManager.ID_COLUMN_INDEX);
				SetUpdated();
			}
		}
		
		throw new RingtoneDoesNotExistException(ringtoneTitle);
	}
	
	/**
	 * @return the Snooze instance of the alarm clock
	 */
	@Override
	public ISnooze GetSnoozeSettings() {
		return m_SnoozeReference.GetReference();
	}
	
	/**
	 * Sets the snooze settings of the alarm clock
	 * @param snoozeSettings the new snooze settings to set
	 */
	@Override
	public void SetSnoozeSettings(ISnooze snoozeSettings) {
		try {
			m_SnoozeReference.Set(snoozeSettings);
			SetUpdated();
		}
		catch (ReferenceCannotBeNullException e) { }
	}
	
	// Public Methods
	
	/**
	 * @return the CREATE TABLE statement of the AlarmClock table
	 */
	static String GetSqlStatementCreateAlarmClockTable() {
		return "CREATE TABLE " + AlarmClockTableName + " (" +
				c_ColumnNameId + 						" INTEGER PRIMARY KEY, " +
				c_AlarmClockColumnNameSettings + 		" INTEGER NOT NULL, " +
				c_AlarmClockColumnNameShutOffMethod + 	" INTEGER NOT NULL, " +
				c_AlarmClockColumnNameRingtone + 		" INTEGER NOT NULL," +
				c_AlarmClockColumnNameSnooze + 			" INTEGER, " +
				"FOREIGN KEY(" + c_AlarmClockColumnNameSnooze + ") REFERENCES " + Snooze.SnoozeTableName + "(" + c_ColumnNameId + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the AlarmClock table
	 */
	static String GetSqlStatementDropAlarmClockTable() {
		return "DROP TABLE IF EXISTS " + AlarmClockTableName;
	}
	
	/**
	 * Queries for an alarm clock instance with the given id
	 * @param context the context of the request (the originating Activity)
	 * @param id the id to query
	 * @return the matching AlarmClock instance with the given id.
	 * @throws DataEntityDoesNotExistException if the AlarmClock does not exist 
	 */
	static AlarmClock GetAlarmClockById(Context context, int id) throws DataEntityDoesNotExistException {
		return GetSingleAlarmClock(
			context,
			c_ColumnNameId + "=" + id);
	}
	
	/**
	 * Queries for an alarm clock instance with the given id
	 * @param context the context of the request (the originating Activity)
	 * @param id the id to query
	 * @return the matching AlarmClock instance with the given id, null if it doesn't exist 
	 */
	static AlarmClock TryGetAlarmClockById(Context context, int id) {
		try {
			return GetAlarmClockById(context, id);
		}
		catch (DataEntityDoesNotExistException e) {
			return null;
		}
	}
	
	/**
	 * Queries for an alarm clock instance with the given snooze settings
	 * @param context the context of the request (the originating Activity)
	 * @param snooze the snooze settings to check
	 * @return the matching AlarmClock instance with the given snooze settings.
	 * @throws DataEntityDoesNotExistException if the AlarmClock does not exist 
	 */
	static AlarmClock GetAlarmClockBySnooze(Context context, ISnooze snooze) throws DataEntityDoesNotExistException {
		return GetSingleAlarmClock(
			context,
			c_AlarmClockColumnNameSnooze + "=" + snooze.GetId());
	}
	
	/**
	 * Queries for an alarm clock instance with the given snooze settings
	 * @param context the context of the request (the originating Activity)
	 * @param snooze the snooze settings to check
	 * @return the matching AlarmClock instance with the given snooze settings, null if it doesn't exist 
	 */
	static AlarmClock TryGetAlarmClockBySnooze(Context context, ISnooze snooze) {
		try {
			return GetAlarmClockBySnooze(context, snooze);
		}
		catch (DataEntityDoesNotExistException e) {
			return null;
		}
	}
	
	// Private Methods
	
	/**
	 * Queries the DB for a single AlarmClock instance using the given where clause.
	 * @param context the context of the request (the originating Activity)
	 * @param whereClause the where clause for the query
	 * @return the matching AlarmClock instance queried from the DB
	 * @throws DataEntityDoesNotExistException if the AlarmClock instance does not exist
	 */
	private static AlarmClock GetSingleAlarmClock(Context context, String whereClause) throws DataEntityDoesNotExistException {
		SQLiteDatabase db = OpenDBForReading(context);
		Cursor cursor = db.query(
			AlarmClockTableName,
			c_QueryAllColumns,
			whereClause,
			null, null, null, null);
		
		try {
			if (!cursor.moveToFirst()) {
				throw new DataEntityDoesNotExistException(AlarmClock.class, whereClause);
			}
			
			return CreateAlarmClockFromCursor(context, cursor);
		}
		finally {
			cursor.close();
			db.close();
		}
	}
	
	/**
	 * Initializes all members of the AlarmClock
	 * @param method the shut off method
	 * @param isActive whether the alarm clock is active
	 * @param isRinging whether the alarm clock should ring
	 * @param isVibrating whether the alarm clock should vibrate
	 * @param ringtoneId the ringtone id
	 * @param snooze the snooze instance
	 */
	private void Initialize(ShutOffMethod method, boolean isActive, boolean isRinging, boolean isVibrating, int ringtoneId, ISnooze snooze) {
		m_Method = method;
		m_RingtoneId = ringtoneId;
		
		m_Settings = EnumSet.noneOf(Settings.class);
		if (isActive) {
			m_Settings.add(Settings.Active);
		}
		
		if (isRinging) {
			m_Settings.add(Settings.Ring);
		}
		
		if (isVibrating) {
			m_Settings.add(Settings.Vibrate);
		}
		
		try {
			m_SnoozeReference.Set(snooze);
		} catch (ReferenceCannotBeNullException e) { }
	}
	
	/**
	 * @return the numeric value of all the settings (bitwise or of all the values)
	 */
	private int GetSettingsValue() {
		int value = 0;
		
		for (Settings setting : m_Settings) {
			value = value | setting.GetValue();
		}
		
		return value;
	}
	
	/**
	 * @param value the value of the settings (bitwise)
	 * @return the matching Settings enum set with the matching values
	 */
	private static EnumSet<Settings> GetSettingsFromValue(int value) {
		EnumSet<Settings> set = EnumSet.noneOf(Settings.class);
		
		if ((value & Settings.Active.GetValue()) != 0) {
			set.add(Settings.Active);
		}
		
		if ((value & Settings.Ring.GetValue()) != 0) {
			set.add(Settings.Ring);
		}
		
		if ((value & Settings.Vibrate.GetValue()) != 0) {
			set.add(Settings.Vibrate);
		}
		
		return set;
	}
	
	/**
	 * Creates a new AlarmClock instance from the values of the record in the cursor
	 * @param context the context of the request (the originating Activity)
	 * @param cursor the DB query cursor placed on an AlarmClock valid record.
	 * @return the corresponding AlarmClock instance
	 */
	private static AlarmClock CreateAlarmClockFromCursor(Context context, Cursor cursor) {
		int id = cursor.getInt(cursor.getColumnIndex(c_ColumnNameId));
		
		int methodValue = cursor.getInt(cursor.getColumnIndex(c_AlarmClockColumnNameShutOffMethod));
		ShutOffMethod method = ShutOffMethod.FromValue(methodValue);
		
		int settingsValue = cursor.getInt(cursor.getColumnIndex(c_AlarmClockColumnNameSettings));
		EnumSet<Settings> settings = GetSettingsFromValue(settingsValue);
		
		int ringtoneId = cursor.getInt(cursor.getColumnIndex(c_AlarmClockColumnNameRingtone));
	
		int snoozeId = cursor.getInt(cursor.getColumnIndex(c_AlarmClockColumnNameSnooze));
		ISnooze snooze = null;
		try {
			snooze = new SnoozeFactory().GetSnoozeById(context, snoozeId);
		// Since the alarm clock instance is queried from the DB, the snooze object cannot be missing
		} catch (DataEntityDoesNotExistException e) { }
		
		return new AlarmClock(
			id,
			method,
			settings.contains(Settings.Active),
			settings.contains(Settings.Ring),
			settings.contains(Settings.Vibrate),
			ringtoneId,
			snooze);
	}
	
	// DataEntity implementation
	
	/**
	 * @return the ContentValues containing the values of the object for the DB
	 */
	@Override
	protected Hashtable<String, ContentValues> GetRecordValues() {
		ContentValues values = new ContentValues();
		
		values.put(c_AlarmClockColumnNameShutOffMethod, m_Method.GetValue());
		values.put(c_AlarmClockColumnNameSettings, GetSettingsValue());
		values.put(c_AlarmClockColumnNameRingtone, m_RingtoneId);
		
		ISnooze snooze = m_SnoozeReference.GetReference();
		if (snooze != null) {
			values.put(c_AlarmClockColumnNameSnooze, snooze.GetId());
		}
		
		Hashtable<String, ContentValues> valuesTable = new Hashtable<String, ContentValues>();
		valuesTable.put(AlarmClockTableName, values);
		return valuesTable;
	}

	/**
	 * @return the name of the table in the DB
	 */
	@Override
	protected Set<String> GetTableNames() {
		Set<String> tableNames = new HashSet<String>();
		tableNames.add(AlarmClockTableName);
		return tableNames;
	}
	
	// IDataEntity Implementation
	
	/**
	 * Loads all members again from the DB and updates the object in memory with the current values.
	 * The loading process uses the object's id.
	 * @param context the context of the request
	 * @throws DataEntityDoesNotExistException if the entity with the current id does not exist in the DB,
	 * or if the object was not yet saved in the DB.
	 */
	@Override
	public void Refresh(Context context) throws DataEntityDoesNotExistException {
		AlarmClock tempAlarmClock = GetAlarmClockById(context, GetId());
		Initialize(
			tempAlarmClock.GetShutOffMethod(),
			tempAlarmClock.IsActive(),
			tempAlarmClock.IsRinging(),
			tempAlarmClock.IsVibrating(),
			tempAlarmClock.GetRingtoneId(),
			tempAlarmClock.GetSnoozeSettings());
	}
	
	/**
	 * Saves the instance in the database.
	 * @param context the context of the request
	 * @throws InvalidSaveDataEntityOperationException if values are invalid
	 */
	@Override
	public void Save(Context context) throws InvalidSaveDataEntityOperationException {
		if (IsUpdated()) {
			// Saving the snooze object before adding it to the alarm clock in the DB.
			m_SnoozeReference.SaveReferences(context);
			
			super.Save(context);
		}
	}
	
	/**
	 * Deletes the instance from the database
	 * @param context the context of the request
	 * @throws InvalidDeleteDataEntityOperationException if the data entity is referenced and cannot be deleted
	 */
	@Override
	public void Delete(Context context) throws InvalidDeleteDataEntityOperationException, InvalidSaveDataEntityOperationException {
		super.Delete(context);
		
		// Handling the deletion of the snooze reference.
		m_SnoozeReference.OnDeleteReferencingEntity(context);
	}
	
	// IReferencingDataEntity implementation
	
	/**
	 * Checks if the given entity is actually referenced by the the referencing entity
	 * @param context the context of the request (the originating Activity)
	 * @param referencedEntity the referenced entity.
	 * @return true if the entity is referenced, otherwise false.
	 */
	@Override
	public boolean IsReferenced(Context context, IReferencedDataEntity referencedEntity) {
		return referencedEntity == m_SnoozeReference.GetReference();
	}

	/**
	 * Checks if the removal of the reference is valid. If not this means that the reference cannot be null.
	 * @param context the context of the request (the originating Activity)
	 * @param referencedEntity the referenced entity.
	 * @return true if the entity can be removed safely, otherwise false.
	 */
	@Override
	public boolean CanRemoveReference(Context context, IReferencedDataEntity referencedEntity) {
		// snooze reference can be null so removing the reference is valid.
		return IsReferenced(context, referencedEntity);
	}
	
	/**
	 * Removes the reference of the referenced data entity.
	 * @param context the context of the request
	 * @param referencedEntity the referenced entity.
	 * @throws InvalidNullReferenceOnDeleteException if the reference cannot be null and the removal is invalid.
	 * @throws InvalidReferenceToRemoveException if the referenced entity is not referenced at all.
	 */
	@Override
	public void RemoveReference(Context context, IReferencedDataEntity referencedEntity) throws InvalidNullReferenceOnDeleteException, InvalidReferenceToRemoveException {
		if (IsReferenced(context, referencedEntity)) {
			SetSnoozeSettings(null);
		}
		else {
			throw new InvalidReferenceToRemoveException(getClass(), GetId(), referencedEntity.getClass(), referencedEntity.GetId());
		}
	}
}
